/*
 * @Author: xiaosihan
 * @Date: 2022-06-08 14:20:52
 * @Last Modified by: xiaosihan
 * @Last Modified time: 2025-01-17 20:42:22
 */

import { clamp, cloneDeep, filter, first, get, isNumber, last } from "lodash";
import { BufferAttribute, BufferGeometry, Material, Matrix4, Mesh, Vector2, Vector3 } from "three";


export type ExtrudePath = {
    vector3: [number, number, number];
    radius: number; // 2 圆角半径
    segments: number; // 圆角分段
    scale: { x: number, y: number } // 1 当前位置对shape的缩放
    name: string; // 备注名字
}

// 挤出
export interface ExtrudeMeshProps {
    paths: Array<ExtrudePath>, // 自动生成路径的
    shapePath: Array<[number, number]>,// 被拉伸成模型的平面路径
    closed: boolean // 是否闭合路径
}

/**
 * 挤出模型
 * 此组件适合自动生成 公路 河流山川等 2d 平面上的模型
 *
 * @export
 * @class ExtrudeMesh
 * @extends {Mesh<BufferGeometry, Material>}
 */
export default class ExtrudeMesh extends Mesh<BufferGeometry, Material> {

    // 临时使用的二维顶点对象
    static temp1Vector2 = new Vector2();
    static temp2Vector2 = new Vector2();
    static temp3Vector2 = new Vector2();

    // 临时使用的3维顶点对象
    static tempVector3 = new Vector3();
    static temp1Vector3 = new Vector3();
    static temp2Vector3 = new Vector3();
    static temp3Vector3 = new Vector3();

    // 临时使用的矩阵
    static temp1Matrix4 = new Matrix4();
    static temp2Matrix4 = new Matrix4();
    static temp3Matrix4 = new Matrix4();

    constructor(props: ExtrudeMeshProps, material?: Material) {
        super(undefined, material);
        this.updateGeometry(props);
    }

    //宽度
    shapePathWidth = 1;

    // 通过 props.paths 和圆角属性 计算出来的 最终路径
    pointerWidthRadius: Array<{
        vector3: [number, number, number], // 点的位置
        matrix: [
            number, number, number, number,
            number, number, number, number,
            number, number, number, number,
            number, number, number, number
        ] // 当前点的变换矩阵
    }> = [];

    // 通过2个顶点和缩放值计算矩阵
    computerMatrix(
        vector1: [number, number, number],
        vector2: [number, number, number],
        scale: [number, number, number],
    ) {
        ExtrudeMesh.temp1Matrix4.identity();
        ExtrudeMesh.temp1Matrix4.multiply(
            ExtrudeMesh.temp2Matrix4.lookAt(
                ExtrudeMesh.temp1Vector3.set(vector1[0], vector1[1], vector1[2]),
                ExtrudeMesh.temp2Vector3.set(vector2[0], vector1[1], vector2[2]),
                ExtrudeMesh.temp3Vector3.set(0, 1, 0)
            )
        );
        ExtrudeMesh.temp1Matrix4.scale(ExtrudeMesh.temp1Vector3.set(...scale));
        return ExtrudeMesh.temp1Matrix4.transpose().toArray() as [
            number, number, number, number,
            number, number, number, number,
            number, number, number, number,
            number, number, number, number
        ];
    }

    /**
     * @计算渐变值 computerGradient([3,2,4], 0.3);
     * @渐变区间 values:number[]
     * @百分比 percentage:number
     */
    computerGradient(values: number[], percentage: number) {
        // 过滤有效数字类型
        values = filter(values, v => isNumber(v));
        // 限定取值范围
        let P = clamp(percentage, 0, 1);
        let VL = values.length;
        for (let i = (VL - 1); i >= 0; i--) {
            if (P === (i / (VL - 1))) {
                return values[i];
            } else if (P > (i / (VL - 1))) {
                P -= (i / (VL - 1));
                P *= 1 / ((i + 1) / (VL - 1) - (i / (VL - 1)));
                return (values[i + 1] - values[i]) * P + values[i];
            }
        }
        return 0;
    }

    // 计算圆角路径 并存到 this.pointerWidthRadius 上
    computerPointerWidthRadius(paths: Array<ExtrudePath>, closed: boolean) {

        // 路径小于2个就不执行计算
        if (paths.length < 2) { return; }

        // 路径少于3个闭合路径参数不起作用
        if (paths.length < 3) {
            closed = false;
        }

        // 清空圆角路径
        this.pointerWidthRadius = [];

        paths = cloneDeep(paths);

        // 如果是闭合路径的话就头尾相连
        if (closed) {
            Object.defineProperties(paths, {
                [-1]: {
                    value: last(paths),
                    configurable: false, // 此属性能否删除和修改
                    enumerable: false, // 此属性能否被遍历
                    writable: false // 此属性的值能否修改
                },
                [paths.length]: {
                    value: first(paths),
                    configurable: false, // 此属性能否删除和修改
                    enumerable: false, // 此属性能否被遍历
                    writable: false // 此属性的值能否修改
                }
            });
        }

        paths.map((P, i) => {

            const {
                vector3,
                radius = 0.1,
                segments = 0,
                scale = { x: 1, y: 1 }
            } = P;

            //当前点位置前后都有点时就可以做圆角计算
            if (paths[i - 1] && paths[i + 1]) {

                // 第一个向量
                let v1 = ExtrudeMesh.temp1Vector3
                    .set(...vector3)
                    .sub(ExtrudeMesh.tempVector3.set(...paths[i - 1].vector3))
                    .normalize()
                    .clone();

                // 第二个向量    
                let v2 = ExtrudeMesh.temp2Vector3
                    .set(...paths[i + 1].vector3)
                    .sub(ExtrudeMesh.tempVector3.set(...vector3))
                    .normalize()
                    .clone();

                // 夹角
                let innerAngle = v1.clone().multiplyScalar(-1).angleTo(v2);
                // 边长
                let sideLength = radius / Math.tan(innerAngle / 2);

                // 圆角的起点
                let startP = new Vector3(...vector3).sub(v1.clone().multiplyScalar(sideLength));
                // 圆角的终点
                let endP = new Vector3(...vector3).add(v2.clone().multiplyScalar(sideLength));

                // 圆角起点到终点的向量
                let StoE = endP.clone().sub(startP);

                // 弦长
                let chordLength = radius / Math.sin(innerAngle / 2);

                // 圆心位置
                let circleCenter = v2
                    .clone()
                    .add(v1)
                    .multiplyScalar(0.5)
                    .sub(v1)
                    .setLength(chordLength)
                    .add(ExtrudeMesh.tempVector3.set(...vector3));

                //向量2是否在 向量1的左边
                let v2Inv1Left = ExtrudeMesh.temp1Vector3
                    .set(-v1.z, 0, v1.x)
                    .angleTo(
                        ExtrudeMesh.temp2Vector3.set(v2.x, 0, v2.z)
                    )
                    < (Math.PI / 2);

                // XZ平面上的夹角
                let innerAngleXZ = ExtrudeMesh.temp1Vector3
                    .set(v1.x, 0, v1.z)
                    .angleTo(
                        ExtrudeMesh.temp2Vector3.set(v2.x, 0, v2.z)
                    );

                // 如果当前点的位置在前后2点的连线上,就不进行分段计算 并且应用上一个点的矩阵
                if (innerAngleXZ === 0) {
                    let { x = 1, y = 1 } = scale;

                    let lastPointMatrix = last(this.pointerWidthRadius)!.matrix;

                    this.pointerWidthRadius.push({
                        vector3,
                        matrix: ExtrudeMesh.temp1Matrix4
                            .identity()
                            .extractRotation(
                                ExtrudeMesh.temp2Matrix4.set(...lastPointMatrix)
                            )
                            .multiply(
                                ExtrudeMesh.temp3Matrix4
                                    .identity()
                                    .scale(
                                        ExtrudeMesh.temp1Vector3.set(1, y, x)
                                    )
                            )
                            .transpose()
                            .toArray()
                    });

                } else if (segments === 0) { // 分段数为0时不分段 并在前后各加一个点

                    let { x = 1, y = 1 } = scale;
               
                    this.pointerWidthRadius.push({
                        vector3,
                        matrix: this.computerMatrix(
                            (v2Inv1Left ? circleCenter.toArray() : vector3),
                            (v2Inv1Left ? vector3 : circleCenter.toArray()),
                            [1, y, x / Math.sin(innerAngle/2)]
                        )
                    });

                } else {

                    for (let ii = 0; ii <= segments; ii++) {
                        // 圆角计算的第n个点的角度
                        let Aa = ii * ((Math.PI - innerAngle) / segments);
                        //半径对角
                        let Cc = Math.PI - Aa - (innerAngle / 2);
                        //圆角分段的弦长
                        let a = radius * Math.sin(Aa) / Math.sin(Cc);
                        // 每段圆角的弦长
                        let StoEvector = StoE.clone().setLength(a).add(startP);

                        // 当前点的位置
                        let ridiusP = StoEvector
                            .clone()
                            .sub(circleCenter)
                            .setLength(radius)
                            .add(circleCenter)

                        let { x: preX = 1, y: preY = 1 } = get(paths[i - 1], ["scale"]) || {};

                        let { x: nextX = 1, y: nextY = 1 } = get(paths[i + 1], ["scale"]) || {};

                        let { x = 1, y = 1 } = scale;

                        // 缩放值过渡计算
                        x = this.computerGradient([(preX + x) / 2, x, (x + nextX) / 2], ii / segments);
                        y = this.computerGradient([(preY + y) / 2, y, (y + nextY) / 2], ii / segments);

                        // 添加一个带矩阵的点
                        this.pointerWidthRadius.push({
                            vector3: ridiusP.toArray(),
                            matrix: this.computerMatrix(
                                (v2Inv1Left ? circleCenter.toArray() : ridiusP.toArray()),
                                (v2Inv1Left ? ridiusP.toArray() : circleCenter.toArray()),
                                [1, y, x]
                            )
                        });
                    }
                }

            } else {

                // 最后一个位置
                if (i === paths.length - 1) {

                    // 如果是闭合的话 就把第一个点的位置加入最后后面
                    if (closed) {
                        this.pointerWidthRadius.push(first(this.pointerWidthRadius)!);

                    } else {

                        // 如果路径未闭合并且遍历到最后一个时特殊处理
                        let lastPoint = last(this.pointerWidthRadius)!;

                        // 在没有闭合的情况下添加第一个点的位置
                        let v = ExtrudeMesh.temp1Vector3
                            .set(...vector3)
                            .sub(
                                ExtrudeMesh.temp3Vector3.set(...lastPoint.vector3)
                            )
                            .normalize()

                        let lookAtPoint = ExtrudeMesh.temp2Vector3
                            .set(v.z, 0, -v.x)
                            .add(
                                ExtrudeMesh.temp3Vector3.set(...vector3)
                            )
                            .toArray();

                        let { x = 1, y = 1 } = scale;

                        this.pointerWidthRadius.push({
                            vector3,
                            matrix: this.computerMatrix(
                                vector3,
                                lookAtPoint,
                                [1, y, x]
                            )
                        });

                    }

                } else {

                    let { x = 1, y = 1 } = scale;

                    // 在没有闭合的情况下添加第一个点的位置
                    let v2 = ExtrudeMesh.temp1Vector3
                        .set(...paths[i + 1].vector3)
                        .sub(
                            ExtrudeMesh.temp2Vector3.set(...vector3)
                        )
                        .normalize();

                    let lookAtPoint = ExtrudeMesh.temp1Vector3
                        .set(v2.z, 0, -v2.x)
                        .add(
                            ExtrudeMesh.temp2Vector3.set(...vector3)
                        )
                        .toArray();

                    this.pointerWidthRadius.push({
                        vector3,
                        matrix: this.computerMatrix(
                            vector3,
                            lookAtPoint,
                            [1, y, x]
                        )
                    });
                }

            } // else

        });

        // 清理圆角路径, 如果2个点相距0.001以内就删除一个点
        // for (let i = 0; i < this.pointerWidthRadius.length; i++) {
        //     while (
        //         this.pointerWidthRadius[i] &&
        //         this.pointerWidthRadius[i + 1] &&
        //         this.computerPathLength(
        //             this.pointerWidthRadius.slice(i, i + 2).map(p => p.vector3)
        //         ) < 0.001
        //     ) {
        //         this.pointerWidthRadius.splice(i, 1);
        //     }
        // }
    }

    // 计算三维路径的总长度
    computerPathLength(points: [number, number, number][]) {
        let length = 0;
        points.map((p, index) => {
            if (index < points.length - 1) {
                ExtrudeMesh.temp1Vector3.set(...p);
                ExtrudeMesh.temp2Vector3.set(...points[index + 1]);
                length += ExtrudeMesh.temp1Vector3.distanceTo(ExtrudeMesh.temp2Vector3);
            }
        });
        return length;
    }

    // 通过一条线段计算挤出模型的各个顶点的位置和 uv坐标
    setExtrudeMesh(
        startP: [number, number, number], // 起点位置
        startMatrix4: [
            number, number, number, number,
            number, number, number, number,
            number, number, number, number,
            number, number, number, number,
        ],// 起点位置的矩阵
        endP: [number, number, number], // 终点位置
        endMatrix4: [
            number, number, number, number,
            number, number, number, number,
            number, number, number, number,
            number, number, number, number,
        ], // 终点位置的矩阵
        uvx: number, // uv.x 的起始位置
        shapePath: Array<[number, number]>
    ) {

        // // 反转路径, 这样uv 坐标看起来习惯一些
        // shapePath = cloneDeep(shapePath).reverse();

        // 计算二维路径的总长度 作为uv坐标的边长
        let shapePathLength = this.computerPathLength(shapePath.map(p => [...p, 0]));
        
        // 挤出长度
        let extrudeLength = this.computerPathLength([startP, endP]);

        // 网格模型每个点
        let geometryPoint: Array<number> = [];

        // 网格模型每个点对应的uv坐标
        let uvPoint: Array<number> = [];

        shapePath.map((P, index) => {
            if (index < shapePath.length - 1) {

                // 计算出矩形面的4个顶点
                //[pa....Pd]
                //[.      .]
                //[.      .]
                //[.      .]
                //[Pb....Pc]
                let Pa = ExtrudeMesh.tempVector3
                    .set(0, P[1], -P[0])
                    .applyMatrix4(
                        ExtrudeMesh.temp1Matrix4.set(...startMatrix4)
                    )
                    .add(
                        ExtrudeMesh.temp1Vector3.set(...startP)
                    )
                    .toArray()

                let Pb = ExtrudeMesh.tempVector3
                    .set(0, shapePath[index + 1][1], -shapePath[index + 1][0])
                    .applyMatrix4(
                        ExtrudeMesh.temp1Matrix4.set(...startMatrix4)
                    )
                    .add(
                        ExtrudeMesh.temp1Vector3.set(...startP)
                    )
                    .toArray()

                let Pc = ExtrudeMesh.tempVector3
                    .set(0, shapePath[index + 1][1], -shapePath[index + 1][0])
                    .applyMatrix4(
                        ExtrudeMesh.temp1Matrix4.set(...endMatrix4)
                    )
                    .add(
                        ExtrudeMesh.temp1Vector3.set(...endP)
                    )
                    .toArray()

                let Pd = ExtrudeMesh.tempVector3
                    .set(0, P[1], -P[0])
                    .applyMatrix4(
                        ExtrudeMesh.temp1Matrix4.set(...endMatrix4)
                    )
                    .add(
                        ExtrudeMesh.temp1Vector3.set(...endP)
                    )
                    .toArray()

                // 组成矩形的2个三角面
                geometryPoint.push(
                    ...Pa, ...Pb, ...Pc,
                    ...Pa, ...Pc, ...Pd
                );

                // 计算出矩形面的4个顶点的uv坐标
                //[Pauv..Pcuv]
                //[.        .]
                //[.        .]
                //[.        .]
                //[Pbuv..Pduv]
                // 当前位置的 uv.y 位置
                let currentUvy = this.computerPathLength(
                    [first(shapePath)!, ...shapePath.slice(0, index + 1)].map(sP => ([...sP, 0] as [number, number, number]))
                ) / shapePathLength;

                // 下一个坐标的 uv.y 位置
                let nextUvy = this.computerPathLength(
                    [first(shapePath)!, ...shapePath.slice(0, index + 2)].map(sP => ([...sP, 0] as [number, number, number]))
                ) / shapePathLength;

                let Pauv = [uvx / shapePathLength, currentUvy];

                let Pbuv = [uvx / shapePathLength, nextUvy];

                let Pcuv = [(uvx + extrudeLength) / shapePathLength, nextUvy];

                let Pduv = [(uvx + extrudeLength) / shapePathLength, currentUvy];

                uvPoint.push(
                    ...Pauv, ...Pbuv, ...Pcuv,
                    ...Pauv, ...Pcuv, ...Pduv
                );
            }
        });

        return { geometryPoint, uvPoint };
    }

    //设置模型
    updateGeometry(props: ExtrudeMeshProps) {

        const { paths, shapePath, closed } = props;
        
        this.shapePathWidth = Math.max(...shapePath.map(path=>path[0])) - Math.min(...shapePath.map(path=>path[0]));
        
        // 计算圆角路径
        this.computerPointerWidthRadius(paths, closed);

        //网格模型的每个顶点位置
        let geometryPoint: Array<number> = [];

        // 网格模型每个点对应的uv坐标
        let uvPoint: number[] = [];

        for (let i = 0; i < this.pointerWidthRadius.length - 1; i++) {

            let Point1 = this.pointerWidthRadius[i];
            let Point2 = this.pointerWidthRadius[i + 1];

            let uvx = this.computerPathLength(this.pointerWidthRadius.slice(0, i + 1).map(p => p.vector3));

            const { geometryPoint: geo, uvPoint: uv } = this.setExtrudeMesh(
                Point1.vector3,
                Point1.matrix,
                Point2.vector3,
                Point2.matrix,
                uvx,
                shapePath
            );

            // 把挤出的网格模型的顶点收集起来
            geometryPoint.push(...geo);

            // 把挤出的网格模型顶点的uv坐标收集起来
            uvPoint.push(...uv);
        }


        let geometry = new BufferGeometry();
        // 修改模型
        let positionArray = new Float32Array(geometryPoint);
        let position = new BufferAttribute(positionArray, 3, false);
        geometry.setAttribute("position", position);

        let uvArray = new Float32Array(uvPoint);
        let uv = new BufferAttribute(uvArray, 2, false);
        geometry.setAttribute("uv", uv);

        // 计算顶点法线
        geometry.computeVertexNormals();

        this.geometry.dispose();
        this.geometry = geometry;

        this.dispatchEvent({ type: "loaded" } as never);
    }


}